两个定时器 setTimeout 和 setInterval
要点速览
shell
# 二者共同点
传统宏任务定时器,异步定时,脱离渲染帧。
# 二者不同点
setTimeout 延迟执行、 setInterval 间歇执行
# 二者都有明显缺点
setTimeout:受主线程阻塞、有 4ms 最小阈值、后台节流,计时不准。
setInterval:除了计时不准,后台节流,还会任务堆积、误差累积、动画卡顿、易内存泄漏,不适合做循环动画和高精度轮询。
# 替代方案
1. 普通轮询 / 倒计时 → 递归 setTimeout(最稳)
2. 动画 / UI 实时更新 → requestAnimationFrame(最流畅)
3. 高精度计时(秒表 / 游戏) → Web Worker(最准)
4. 低优先级后台任务 → scheduler.postTask(最省性能)
# 总结
不可见、非活跃、被遮挡、切标签、最小化、锁屏 → 都是后台 → 定时器被节流到 1 秒左右一次 → 计时不准、累积误差。
setTimeout 是单纯的不准有误差
setInterval 本来就有任务堆积,再加后台节流,误差更离谱
总的来说
setInterval 在主线程中完全不可用,只能在 Web worker 里没问题
setTimeout 可以模拟稳定的间歇性定时,但是涉及到前后台切换的时候需要手动关闭和开启定时,需要判断是否进入后台,监听 visibilitychange 事件
准时计时,不受前后台切换影响的情况下,可用 Web Worker 来代替拓展
shell
# setTimeout 缺点
setInterval 最大问题是任务堆积
1. 计时不准、存在延迟
JS 单线程,若主线程有同步耗时任务,必须等主线程空闲才会执行,设定时间只是「最早执行时间」,不是准时执行。
2. 最小时间间隔限制
浏览器对嵌套定时器有最小 4ms 阈值,即使设 0ms 也不是立即执行,会被强制延后。
3. 依赖事件循环队列
受宏任务排队影响,渲染、其他脚本阻塞都会拉长实际延时。
4. 标签页后台被节流
页面隐藏 / 切后台时,浏览器会大幅降低定时器频率,计时严重不准。
# setInterval 缺点(更严重)
setInterval 最大问题是任务堆积
1. 同样计时不准
和 setTimeout 一样,受主线程阻塞、后台节流影响,间隔不稳定。
2. 任务堆积、丢帧 / 叠加执行
不管上一次任务有没有执行完,到点就压任务,容易任务堆积;
如果回调执行耗时 > 设定间隔,会造成任务排队堆积、连续扎堆执行,逻辑混乱、页面卡顿。
3. 无法和浏览器渲染帧对齐
和屏幕刷新率不同步,做动画会卡顿、掉帧、抖动。
4. 误差会累积
每次执行都有微小延迟,多次循环后时间误差越来越大。
# 解决方案
1. 递归 setTimeout(最通用、最推荐)
原理:每次执行完再开启下一次定时器,绝对不会堆积。
优点:稳定、无堆积、兼容所有浏览器
缺点:仍然受主线程阻塞影响
适合:轮询、倒计时、普通定时
2. requestAnimationFrame(RAF)—— 做动画 / UI 首选
原理:和浏览器刷新频率对齐(60 帧≈16ms),页面隐藏自动暂停。
优点:超流畅、不掉帧,后台自动暂停,省电,时间更精准
缺点:不能自定义毫秒间隔(固定 16ms 左右)
适用:JS 动画、进度条、时钟、拖拽、UI 实时更新
3. Web Worker(真正精准定时,不受主线程阻塞)
原理:把定时器放到独立线程运行,完全不卡主线程,最精准。
优点:真正高精度,主线程再卡也不影响计时
缺点:不能操作 DOM、使用稍复杂,可以通过与主线程发布事件来解决操作 dom 的问题
适用:高精度计时器、秒表、游戏、直播计时
4. scheduler.postTask —— 现代浏览器新 API(优先级调度)
原理:给任务设置优先级,空闲时执行,不阻塞渲染。
优点:性能友好、不卡顿
缺点:兼容性一般
适用:低优先级轮询、后台统计、日志上报setTimeout 递归 替代 setInterval
递归 setTimeout:等上一次回调执行完,再开启下一次延时,彻底解决堆积、误差累积。
js
/**
* 递归版定时器 替代 setInterval
* @param {Function} callback 执行回调
* @param {number} delay 间隔毫秒
* @returns {Object} 包含启动、停止方法
*/
function createTimer(callback, delay) {
let timerId = null;
let isRunning = false;
// 递归执行
function loop() {
if (!isRunning) return;
callback();
timerId = setTimeout(loop, delay);
}
return {
start() {
if (isRunning) return;
isRunning = true;
loop();
},
stop() {
isRunning = false;
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
}
};
}
// 使用:创建定时器,间隔 1000ms
const timer = createTimer(() => {
console.log('定时执行', Date.now());
}, 1000);
// 启动
timer.start();
// 需要时停止
// timer.stop();Web Worker 实现准时秒表
js
/************** dom **************/
<div id="time">00:00.00</div>
/************** 主线程 js只需要做: 监听 Worker 事件来操作 dom **************/
// 1. 创建 Worker
const worker = new Worker('./timer.worker.js');
const timeEl = document.getElementById('time');
// 2. 接收 Worker 发来的时间,更新 DOM
worker.onmessage = (e) => {
const { ms } = e.data;
const formatTime = formatMs(ms);
timeEl.innerText = formatTime; // 只在这里操作 DOM
};
// 3. 开始计时
worker.postMessage({ type: 'start' });
// 格式化时间
function formatMs(ms) {
const minutes = String(Math.floor(ms / 60000)).padStart(2, '0');
const seconds = String(Math.floor((ms % 60000) / 1000)).padStart(2, '0');
const cent = String(Math.floor((ms % 1000) / 10)).padStart(2, '0');
return `${minutes}:${seconds}.${cent}`;
}
/************** Worker 线程(timer.worker.js)—— 只负责高精度计时 **************/
let startTime = 0;
let rafId = null;
// 接收主线程指令
self.onmessage = (e) => {
if (e.data.type === 'start') {
start();
}
};
function start() {
startTime = performance.now();
loop();
}
// 高精度循环(Worker 里不受主线程阻塞)
function loop() {
const now = performance.now();
const ms = Math.floor(now - startTime); // 计算已过时间
// 发送时间给主线程更新 DOM
self.postMessage({ ms });
// 继续循环
rafId = requestAnimationFrame(loop);
}切换到后台指的是什么?
js
setInterval / setTimeout 二者切后台后都会出现严重计时不准问题,其实是浏览器主线程优化节流所致
// 切换到后台的场景
// 1. 浏览器内(最常见)
✅ 切换到别的标签页(当前标签不可见)
✅ 浏览器窗口最小化
✅ 窗口被其他窗口完全遮挡(页面不可见)
✅ 手机上切到别的 App / 锁屏(WebView / 浏览器)
// 2. 页面自身状态
✅ document.hidden === true(页面可见性 API 判断)
✅ 窗口失焦(blur),且页面不可见
// 3. 系统级
✅ 电脑锁屏 / 睡眠
✅ 手机锁屏 / 息屏
// 哪些情况「不算后台」(不节流)?
❌ 页面可见、当前标签、窗口没最小化
❌ 页面在播放音视频(浏览器认为你在用,不节流)
❌ 用 Web Worker 里的定时器(Worker 不受主线程节流影响)
❌ 用 requestAnimationFrame:后台直接暂停,切回才继续(不是变慢)
// 怎么用代码判断「是否后台」?
console.log(document.hidden);
// true = 后台/不可见;false = 前台/可见
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('进入后台,定时器会变慢');
} else {
console.log('切回前台,恢复正常');
}
});